/*! RowReorder 1.2.7
 * 2015-2020 SpryMedia Ltd - datatables.net/license
 */

/**
 * @summary     RowReorder
 * @description Row reordering extension for DataTables
 * @version     1.2.7
 * @file        dataTables.rowReorder.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     www.sprymedia.co.uk/contact
 * @copyright   Copyright 2015-2020 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net
 */

(function (factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery', 'datatables.net'], function ($) {
      return factory($, window, document);
    });
  } else if (typeof exports === 'object') {
    // CommonJS
    module.exports = function (root, $) {
      if (!root) {
        root = window;
      }

      if (!$ || !$.fn.dataTable) {
        $ = require('datatables.net')(root, $).$;
      }

      return factory($, root, root.document);
    };
  } else {
    // Browser
    factory(jQuery, window, document);
  }
}(function ($, window, document, undefined) {
  'use strict';
  var DataTable = $.fn.dataTable;


  /**
   * RowReorder provides the ability in DataTables to click and drag rows to
   * reorder them. When a row is dropped the data for the rows effected will be
   * updated to reflect the change. Normally this data point should also be the
   * column being sorted upon in the DataTable but this does not need to be the
   * case. RowReorder implements a "data swap" method - so the rows being
   * reordered take the value of the data point from the row that used to occupy
   * the row's new position.
   *
   * Initialisation is done by either:
   *
   * * `rowReorder` parameter in the DataTable initialisation object
   * * `new $.fn.dataTable.RowReorder( table, opts )` after DataTables
   *   initialisation.
   *
   *  @class
   *  @param {object} settings DataTables settings object for the host table
   *  @param {object} [opts] Configuration options
   *  @requires jQuery 1.7+
   *  @requires DataTables 1.10.7+
   */
  var RowReorder = function (dt, opts) {
    // Sanity check that we are using DataTables 1.10 or newer
    if (!DataTable.versionCheck || !DataTable.versionCheck('1.10.8')) {
      throw 'DataTables RowReorder requires DataTables 1.10.8 or newer';
    }

    // User and defaults configuration object
    this.c = $.extend(true, {},
      DataTable.defaults.rowReorder,
      RowReorder.defaults,
      opts
    );

    // Internal settings
    this.s = {
      /** @type {integer} Scroll body top cache */
      bodyTop: null,

      /** @type {DataTable.Api} DataTables' API instance */
      dt: new DataTable.Api(dt),

      /** @type {function} Data fetch function */
      getDataFn: DataTable.ext.oApi._fnGetObjectDataFn(this.c.dataSrc),

      /** @type {array} Pixel positions for row insertion calculation */
      middles: null,

      /** @type {Object} Cached dimension information for use in the mouse move event handler */
      scroll: {},

      /** @type {integer} Interval object used for smooth scrolling */
      scrollInterval: null,

      /** @type {function} Data set function */
      setDataFn: DataTable.ext.oApi._fnSetObjectDataFn(this.c.dataSrc),

      /** @type {Object} Mouse down information */
      start: {
        top: 0,
        left: 0,
        offsetTop: 0,
        offsetLeft: 0,
        nodes: []
      },

      /** @type {integer} Window height cached value */
      windowHeight: 0,

      /** @type {integer} Document outer height cached value */
      documentOuterHeight: 0,

      /** @type {integer} DOM clone outer height cached value */
      domCloneOuterHeight: 0
    };

    // DOM items
    this.dom = {
      /** @type {jQuery} Cloned row being moved around */
      clone: null,

      /** @type {jQuery} DataTables scrolling container */
      dtScroll: $('div.dataTables_scrollBody', this.s.dt.table().container())
    };

    // Check if row reorder has already been initialised on this table
    var settings = this.s.dt.settings()[0];
    var exisiting = settings.rowreorder;

    if (exisiting) {
      return exisiting;
    }

    if (!this.dom.dtScroll.length) {
      this.dom.dtScroll = $(this.s.dt.table().container(), 'tbody')
    }

    settings.rowreorder = this;
    this._constructor();
  };


  $.extend(RowReorder.prototype, {
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Constructor
     */

    /**
     * Initialise the RowReorder instance
     *
     * @private
     */
    _constructor: function () {
      var that = this;
      var dt = this.s.dt;
      var table = $(dt.table().node());

      // Need to be able to calculate the row positions relative to the table
      if (table.css('position') === 'static') {
        table.css('position', 'relative');
      }

      // listen for mouse down on the target column - we have to implement
      // this rather than using HTML5 drag and drop as drag and drop doesn't
      // appear to work on table rows at this time. Also mobile browsers are
      // not supported.
      // Use `table().container()` rather than just the table node for IE8 -
      // otherwise it only works once...
      $(dt.table().container()).on('mousedown.rowReorder touchstart.rowReorder', this.c.selector, function (e) {
        if (!that.c.enable) {
          return;
        }

        // Ignore excluded children of the selector
        if ($(e.target).is(that.c.excludedChildren)) {
          return true;
        }

        var tr = $(this).closest('tr');
        var row = dt.row(tr);

        // Double check that it is a DataTable row
        if (row.any()) {
          that._emitEvent('pre-row-reorder', {
            node: row.node(),
            index: row.index()
          });

          that._mouseDown(e, tr);
          return false;
        }
      });

      dt.on('destroy.rowReorder', function () {
        $(dt.table().container()).off('.rowReorder');
        dt.off('.rowReorder');
      });
    },


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Private methods
     */

    /**
     * Cache the measurements that RowReorder needs in the mouse move handler
     * to attempt to speed things up, rather than reading from the DOM.
     *
     * @private
     */
    _cachePositions: function () {
      var dt = this.s.dt;

      // Frustratingly, if we add `position:relative` to the tbody, the
      // position is still relatively to the parent. So we need to adjust
      // for that
      var headerHeight = $(dt.table().node()).find('thead').outerHeight();

      // Need to pass the nodes through jQuery to get them in document order,
      // not what DataTables thinks it is, since we have been altering the
      // order
      var nodes = $.unique(dt.rows({page: 'current'}).nodes().toArray());
      var middles = $.map(nodes, function (node, i) {
        var top = $(node).position().top - headerHeight;

        return (top + top + $(node).outerHeight()) / 2;
      });

      this.s.middles = middles;
      this.s.bodyTop = $(dt.table().body()).offset().top;
      this.s.windowHeight = $(window).height();
      this.s.documentOuterHeight = $(document).outerHeight();
    },


    /**
     * Clone a row so it can be floated around the screen
     *
     * @param  {jQuery} target Node to be cloned
     * @private
     */
    _clone: function (target) {
      var dt = this.s.dt;
      var clone = $(dt.table().node().cloneNode(false))
        .addClass('dt-rowReorder-float')
        .append('<tbody/>')
        .append(target.clone(false));

      // Match the table and column widths - read all sizes before setting
      // to reduce reflows
      var tableWidth = target.outerWidth();
      var tableHeight = target.outerHeight();
      var sizes = target.children().map(function () {
        return $(this).width();
      });

      clone
        .width(tableWidth)
        .height(tableHeight)
        .find('tr').children().each(function (i) {
        this.style.width = sizes[i] + 'px';
      });

      // Insert into the document to have it floating around
      clone.appendTo('body');

      this.dom.clone = clone;
      this.s.domCloneOuterHeight = clone.outerHeight();
    },


    /**
     * Update the cloned item's position in the document
     *
     * @param  {object} e Event giving the mouse's position
     * @private
     */
    _clonePosition: function (e) {
      var start = this.s.start;
      var topDiff = this._eventToPage(e, 'Y') - start.top;
      var leftDiff = this._eventToPage(e, 'X') - start.left;
      var snap = this.c.snapX;
      var left;
      var top = topDiff + start.offsetTop;

      if (snap === true) {
        left = start.offsetLeft;
      } else if (typeof snap === 'number') {
        left = start.offsetLeft + snap;
      } else {
        left = leftDiff + start.offsetLeft;
      }

      if (top < 0) {
        top = 0
      } else if (top + this.s.domCloneOuterHeight > this.s.documentOuterHeight) {
        top = this.s.documentOuterHeight - this.s.domCloneOuterHeight;
      }

      this.dom.clone.css({
        top: top,
        left: left
      });
    },


    /**
     * Emit an event on the DataTable for listeners
     *
     * @param  {string} name Event name
     * @param  {array} args Event arguments
     * @private
     */
    _emitEvent: function (name, args) {
      this.s.dt.iterator('table', function (ctx, i) {
        $(ctx.nTable).triggerHandler(name + '.dt', args);
      });
    },


    /**
     * Get pageX/Y position from an event, regardless of if it is a mouse or
     * touch event.
     *
     * @param  {object} e Event
     * @param  {string} pos X or Y (must be a capital)
     * @private
     */
    _eventToPage: function (e, pos) {
      if (e.type.indexOf('touch') !== -1) {
        return e.originalEvent.touches[0]['page' + pos];
      }

      return e['page' + pos];
    },


    /**
     * Mouse down event handler. Read initial positions and add event handlers
     * for the move.
     *
     * @param  {object} e      Mouse event
     * @param  {jQuery} target TR element that is to be moved
     * @private
     */
    _mouseDown: function (e, target) {
      var that = this;
      var dt = this.s.dt;
      var start = this.s.start;

      var offset = target.offset();
      start.top = this._eventToPage(e, 'Y');
      start.left = this._eventToPage(e, 'X');
      start.offsetTop = offset.top;
      start.offsetLeft = offset.left;
      start.nodes = $.unique(dt.rows({page: 'current'}).nodes().toArray());

      this._cachePositions();
      this._clone(target);
      this._clonePosition(e);

      this.dom.target = target;
      target.addClass('dt-rowReorder-moving');

      $(document)
        .on('mouseup.rowReorder touchend.rowReorder', function (e) {
          that._mouseUp(e);
        })
        .on('mousemove.rowReorder touchmove.rowReorder', function (e) {
          that._mouseMove(e);
        });

      // Check if window is x-scrolling - if not, disable it for the duration
      // of the drag
      if ($(window).width() === $(document).width()) {
        $(document.body).addClass('dt-rowReorder-noOverflow');
      }

      // Cache scrolling information so mouse move doesn't need to read.
      // This assumes that the window and DT scroller will not change size
      // during an row drag, which I think is a fair assumption
      var scrollWrapper = this.dom.dtScroll;
      this.s.scroll = {
        windowHeight: $(window).height(),
        windowWidth: $(window).width(),
        dtTop: scrollWrapper.length ? scrollWrapper.offset().top : null,
        dtLeft: scrollWrapper.length ? scrollWrapper.offset().left : null,
        dtHeight: scrollWrapper.length ? scrollWrapper.outerHeight() : null,
        dtWidth: scrollWrapper.length ? scrollWrapper.outerWidth() : null
      };
    },


    /**
     * Mouse move event handler - move the cloned row and shuffle the table's
     * rows if required.
     *
     * @param  {object} e Mouse event
     * @private
     */
    _mouseMove: function (e) {
      this._clonePosition(e);

      // Transform the mouse position into a position in the table's body
      var bodyY = this._eventToPage(e, 'Y') - this.s.bodyTop;
      var middles = this.s.middles;
      var insertPoint = null;
      var dt = this.s.dt;

      // Determine where the row should be inserted based on the mouse
      // position
      for (var i = 0, ien = middles.length; i < ien; i++) {
        if (bodyY < middles[i]) {
          insertPoint = i;
          break;
        }
      }

      if (insertPoint === null) {
        insertPoint = middles.length;
      }

      // Perform the DOM shuffle if it has changed from last time
      if (this.s.lastInsert === null || this.s.lastInsert !== insertPoint) {
        var nodes = $.unique(dt.rows({page: 'current'}).nodes().toArray());

        if (insertPoint > this.s.lastInsert) {
          this.dom.target.insertAfter(nodes[insertPoint - 1]);
        } else {
          this.dom.target.insertBefore(nodes[insertPoint]);
        }

        this._cachePositions();

        this.s.lastInsert = insertPoint;
      }

      this._shiftScroll(e);
    },


    /**
     * Mouse up event handler - release the event handlers and perform the
     * table updates
     *
     * @param  {object} e Mouse event
     * @private
     */
    _mouseUp: function (e) {
      var that = this;
      var dt = this.s.dt;
      var i, ien;
      var dataSrc = this.c.dataSrc;

      this.dom.clone.remove();
      this.dom.clone = null;

      this.dom.target.removeClass('dt-rowReorder-moving');
      //this.dom.target = null;

      $(document).off('.rowReorder');
      $(document.body).removeClass('dt-rowReorder-noOverflow');

      clearInterval(this.s.scrollInterval);
      this.s.scrollInterval = null;

      // Calculate the difference
      var startNodes = this.s.start.nodes;
      var endNodes = $.unique(dt.rows({page: 'current'}).nodes().toArray());
      var idDiff = {};
      var fullDiff = [];
      var diffNodes = [];
      var getDataFn = this.s.getDataFn;
      var setDataFn = this.s.setDataFn;

      for (i = 0, ien = startNodes.length; i < ien; i++) {
        if (startNodes[i] !== endNodes[i]) {
          var id = dt.row(endNodes[i]).id();
          var endRowData = dt.row(endNodes[i]).data();
          var startRowData = dt.row(startNodes[i]).data();

          if (id) {
            idDiff[id] = getDataFn(startRowData);
          }

          fullDiff.push({
            node: endNodes[i],
            oldData: getDataFn(endRowData),
            newData: getDataFn(startRowData),
            newPosition: i,
            oldPosition: $.inArray(endNodes[i], startNodes)
          });

          diffNodes.push(endNodes[i]);
        }
      }

      // Create event args
      var eventArgs = [fullDiff, {
        dataSrc: dataSrc,
        nodes: diffNodes,
        values: idDiff,
        triggerRow: dt.row(this.dom.target),
        originalEvent: e
      }];

      // Emit event
      this._emitEvent('row-reorder', eventArgs);

      var update = function () {
        if (that.c.update) {
          for (i = 0, ien = fullDiff.length; i < ien; i++) {
            var row = dt.row(fullDiff[i].node);
            var rowData = row.data();

            setDataFn(rowData, fullDiff[i].newData);

            // Invalidate the cell that has the same data source as the dataSrc
            dt.columns().every(function () {
              if (this.dataSrc() === dataSrc) {
                dt.cell(fullDiff[i].node, this.index()).invalidate('data');
              }
            });
          }

          // Trigger row reordered event
          that._emitEvent('row-reordered', eventArgs);

          dt.draw(false);
        }
      };

      // Editor interface
      if (this.c.editor) {
        // Disable user interaction while Editor is submitting
        this.c.enable = false;

        this.c.editor
          .edit(
            diffNodes,
            false,
            $.extend({submit: 'changed'}, this.c.formOptions)
          )
          .multiSet(dataSrc, idDiff)
          .one('preSubmitCancelled.rowReorder', function () {
            that.c.enable = true;
            that.c.editor.off('.rowReorder');
            dt.draw(false);
          })
          .one('submitUnsuccessful.rowReorder', function () {
            dt.draw(false);
          })
          .one('submitSuccess.rowReorder', function () {
            update();
          })
          .one('submitComplete', function () {
            that.c.enable = true;
            that.c.editor.off('.rowReorder');
          })
          .submit();
      } else {
        update();
      }
    },


    /**
     * Move the window and DataTables scrolling during a drag to scroll new
     * content into view.
     *
     * This matches the `_shiftScroll` method used in AutoFill, but only
     * horizontal scrolling is considered here.
     *
     * @param  {object} e Mouse move event object
     * @private
     */
    _shiftScroll: function (e) {
      var that = this;
      var dt = this.s.dt;
      var scroll = this.s.scroll;
      var runInterval = false;
      var scrollSpeed = 5;
      var buffer = 65;
      var
        windowY = e.pageY - document.body.scrollTop,
        windowVert,
        dtVert;

      // Window calculations - based on the mouse position in the window,
      // regardless of scrolling
      if (windowY < $(window).scrollTop() + buffer) {
        windowVert = scrollSpeed * -1;
      } else if (windowY > scroll.windowHeight + $(window).scrollTop() - buffer) {
        windowVert = scrollSpeed;
      }

      // DataTables scrolling calculations - based on the table's position in
      // the document and the mouse position on the page
      if (scroll.dtTop !== null && e.pageY < scroll.dtTop + buffer) {
        dtVert = scrollSpeed * -1;
      } else if (scroll.dtTop !== null && e.pageY > scroll.dtTop + scroll.dtHeight - buffer) {
        dtVert = scrollSpeed;
      }

      // This is where it gets interesting. We want to continue scrolling
      // without requiring a mouse move, so we need an interval to be
      // triggered. The interval should continue until it is no longer needed,
      // but it must also use the latest scroll commands (for example consider
      // that the mouse might move from scrolling up to scrolling left, all
      // with the same interval running. We use the `scroll` object to "pass"
      // this information to the interval. Can't use local variables as they
      // wouldn't be the ones that are used by an already existing interval!
      if (windowVert || dtVert) {
        scroll.windowVert = windowVert;
        scroll.dtVert = dtVert;
        runInterval = true;
      } else if (this.s.scrollInterval) {
        // Don't need to scroll - remove any existing timer
        clearInterval(this.s.scrollInterval);
        this.s.scrollInterval = null;
      }

      // If we need to run the interval to scroll and there is no existing
      // interval (if there is an existing one, it will continue to run)
      if (!this.s.scrollInterval && runInterval) {
        this.s.scrollInterval = setInterval(function () {
          // Don't need to worry about setting scroll <0 or beyond the
          // scroll bound as the browser will just reject that.
          if (scroll.windowVert) {
            var top = $(document).scrollTop();
            $(document).scrollTop(top + scroll.windowVert);

            if (top !== $(document).scrollTop()) {
              var move = parseFloat(that.dom.clone.css("top"));
              that.dom.clone.css("top", move + scroll.windowVert);
            }
          }

          // DataTables scrolling
          if (scroll.dtVert) {
            var scroller = that.dom.dtScroll[0];

            if (scroll.dtVert) {
              scroller.scrollTop += scroll.dtVert;
            }
          }
        }, 20);
      }
    }
  });


  /**
   * RowReorder default settings for initialisation
   *
   * @namespace
   * @name RowReorder.defaults
   * @static
   */
  RowReorder.defaults = {
    /**
     * Data point in the host row's data source object for where to get and set
     * the data to reorder. This will normally also be the sorting column.
     *
     * @type {Number}
     */
    dataSrc: 0,

    /**
     * Editor instance that will be used to perform the update
     *
     * @type {DataTable.Editor}
     */
    editor: null,

    /**
     * Enable / disable RowReorder's user interaction
     * @type {Boolean}
     */
    enable: true,

    /**
     * Form options to pass to Editor when submitting a change in the row order.
     * See the Editor `from-options` object for details of the options
     * available.
     * @type {Object}
     */
    formOptions: {},

    /**
     * Drag handle selector. This defines the element that when dragged will
     * reorder a row.
     *
     * @type {String}
     */
    selector: 'td:first-child',

    /**
     * Optionally lock the dragged row's x-position. This can be `true` to
     * fix the position match the host table's, `false` to allow free movement
     * of the row, or a number to define an offset from the host table.
     *
     * @type {Boolean|number}
     */
    snapX: false,

    /**
     * Update the table's data on drop
     *
     * @type {Boolean}
     */
    update: true,

    /**
     * Selector for children of the drag handle selector that mouseDown events
     * will be passed through to and drag will not activate
     *
     * @type {String}
     */
    excludedChildren: 'a'
  };


  /*
   * API
   */
  var Api = $.fn.dataTable.Api;

// Doesn't do anything - work around for a bug in DT... Not documented
  Api.register('rowReorder()', function () {
    return this;
  });

  Api.register('rowReorder.enable()', function (toggle) {
    if (toggle === undefined) {
      toggle = true;
    }

    return this.iterator('table', function (ctx) {
      if (ctx.rowreorder) {
        ctx.rowreorder.c.enable = toggle;
      }
    });
  });

  Api.register('rowReorder.disable()', function () {
    return this.iterator('table', function (ctx) {
      if (ctx.rowreorder) {
        ctx.rowreorder.c.enable = false;
      }
    });
  });


  /**
   * Version information
   *
   * @name RowReorder.version
   * @static
   */
  RowReorder.version = '1.2.6';


  $.fn.dataTable.RowReorder = RowReorder;
  $.fn.DataTable.RowReorder = RowReorder;

// Attach a listener to the document which listens for DataTables initialisation
// events so we can automatically initialise
  $(document).on('init.dt.dtr', function (e, settings, json) {
    if (e.namespace !== 'dt') {
      return;
    }

    var init = settings.oInit.rowReorder;
    var defaults = DataTable.defaults.rowReorder;

    if (init || defaults) {
      var opts = $.extend({}, init, defaults);

      if (init !== false) {
        new RowReorder(settings, opts);
      }
    }
  });


  return RowReorder;
}));
